/*
 * Copyright (c) 2010 MediaMonks
 *
 * MediaMonks Hyves JS
 *
 * A fairly easy to use implementation of the Hyves Data API.
 *
 * This class requires the SHA-1 algorithm which can
 * be downloaded from: http://pajhome.org.uk/crypt/md5/
 * 
 * Documentation about the Hyves Data API can be found here:
 * http://www.hyves-developers.nl/documentation/data-api/home
 *
 * Thanks to Kilian Marjew for his GenusApi which was the
 * inspiration for this class, more info about the
 * GenusApi at http://hyvapi.marjew.nl/
 *
 * @version 1.0 alpha 3
 * @author Robert Slootjes <robert@mediamonks.com>
 * @url http://code.google.com/p/mediamonks-hyvesjs/
 * @license	LGPL http://en.wikipedia.org/wiki/GNU_Lesser_General_Public_License
 *
 * @todo finish/update docs
 */
var cbFunc = {};

/**
 * Hyves
 *
 * @param {string} consumerKey
 * @param {string} consumerSecret
 * @param {object} options (optional)
 */
var Hyves = function(consumerKey, consumerSecret, options)
{
	this._urlApi = 'http://data.hyves-api.nl/';

	this._version = '2.0';
	this._fancylayout = true;
	this._format = 'json';
	this._alwaysReturnCode200 = true;
	this._signatureMethod = 'HMAC-SHA1';
	this._httpRequestMethod = 'GET';

	this._consumerKey = consumerKey;
	this._consumerSecret = consumerSecret;

	this._token = '';
	this._tokenSecret = '';
	this._tokenExpireDate = 0;
	this._tokenMethods = new Array();
	this._tokenUserId = 0;

	this._isBatch = false;
	this._batchStack = new Array();

	this._callbackId = 0;
	this.OAuth = new OAuth();

	/**
	 * setVersion
	 * more about versions:
	 * http://www.hyves-developers.nl/version/2.0
	 *
	 * @param {string} version
	 */
	this.setVersion = function(version) {
		this._version = version;
	}

	/**
	 * setFancyLayout
	 * 
	 * more about fancy layout:
	 * http://www.hyves-developers.nl/documentation/data-api/fancy-layout
	 *
	 * @param {boolean} fancylayout
	 */
	this.setFancyLayout = function(fancylayout) {
		this._fancylayout = fancylayout;
	}

	/**
	 * setToken
	 *
	 * adds the user token to the calls
	 *
	 * @param {string} token
	 * @param {string} tokenSecret
	 */
	this.setToken = function(token, tokenSecret) {
		this._token = token;
		this._tokenSecret = tokenSecret;
	}
	
	/**
	 * setTokenExpireDate
	 *
	 * @param {integer} expiretimestamp
	 */
	this.setTokenExpireDate = function(expire)
	{
		this._tokenExpireDate = expire; 
	}
	
	/**
	 * setTokenMethods
	 *
	 * set methods the token is valid for
	 *
	 * @param {array} methods
	 */
	this.setTokenMethods = function(methods)
	{
		this._tokenMethods = methods; 
	}
	
	/**
	 * setTokenUserId
	 *
	 * set userid the token is valid for
	 *
	 * @param {array} methods
	 */
	this.setTokenUserId = function(userId)
	{
		this._tokenUserId = userId;	
	}
	
	/**
	 * getToken
	 *
	 * get current token
	 *
	 * @return string
	 */
	this.getToken = function()
	{
		return this._token;	
	}
	
	/**
	 * getTokenSecret
	 *
	 * get current token secret
	 *
	 * @return string
	 */
	this.getTokenSecret = function()
	{
		return this._tokenSecret;
	}
	
	/**
	 * getTokenExpireDate
	 *
	 * @return {integer}
	 */
	this.getTokenExpireDate = function()
	{
		return this._tokenExpireDate; 
	}
	
	/**
	 * setTokenMethods
	 *
	 * get methods the token is valid for
	 *
	 * @return {array}
	 */
	this.getTokenMethods = function()
	{
		return this._tokenMethods; 
	}
	
	/**
	 * getUserId
	 *
	 * get user id
	 *
	 * @return {string}
	 */
	this.getTokenUserId = function()
	{
		return this._tokenUserId; 
	}

	/**
	 * getNextCallbackId
	 *
	 * returns the next callback id
	 *
	 * @return integer
	 */
	this.getNextCallbackId = function() {
		return ++this._callbackId;
	}

	/**
	 * call
	 *
	 * @param {string} method
	 * @param {object} params
	 * @param {function} callback
	 */
	this.call = function(method, params, callback) {
		if(this._isBatch == true) {
			if(this._batchStack.length == 10) {
				throw new Hyves_Exception('maximum of 10 calls reached', 20);
			}
			if(typeof params == 'function') {
				throw new Hyves_Exception('callback should not be specified', 21);
			}
			this._batchStack.push(new Hyves.Request(this, method, params));
		}
		else {
			var request = new Hyves.Request(this, method, params, callback);
			this.executeSingle(request);
		}
	}

	/**
	 * startBatch
	 *
	 * start batch mode, called method won't be
	 * executed untill flushBatch() is called.
	 *
	 * more about batch processing:
	 * http://www.hyves-developers.nl/documentation/data-api/batch-processing
	 */
	this.startBatch = function() {
		this._batchStack = new Array();
		this._isBatch = true;
	}

	/**
	 * flushBatch
	 *
	 * execute stacked calls
	 *
	 * @param {object} callback
	 */
	this.flushBatch = function(callback) {
		if(this._batchStack.length == 0) {
			throw new Hyves_Exception('no calls are queued', 22);
		}
		this.executeBatch(this._batchStack, callback);
	}

	/**
	 * executeSingle
	 *
	 * executes the call, created a <script>-tag and
	 * calls the specified callback method
	 *
	 * @param {object} request, a Hyves.Request object
	 */
	this.executeSingle = function(request) {
		try {
			var callbackId = request.getCallbackId();
			var callback = request.getCallbackFunction();
			var url = this._urlApi + '?' + request.toParams();
			var scriptId = '_callback_' + callbackId + '_';
			var self = this;

			cbFunc['o' + callbackId] = function(response) {
				callback(self.createResponse(response));

				delete cbFunc['o' + callbackId];
				var el = document.getElementById(scriptId);
				el.parentNode.removeChild(el);
			};

			var script = document.createElement('script');
			script.id = scriptId;
			script.src = url;

			var head = document.getElementsByTagName("head").item(0);
			head.appendChild(script);
		}
		catch(e) {
			throw new Hyves_Request_Exception(e, 1);
		}
	}

	/**
	 * executeBatch
	 *
	 * created a single request from the stacked calls
	 * and executes it trough executeSingle()
	 *
	 * @param {array} requestStack
	 * @param {function} callback method
	 */
	this.executeBatch = function(requestStack, callback) {
		this._isBatch = false;
		this._callback = callback;

		var request = new Array();
		var requestCount = requestStack.length;
		for(i = 0; i < requestCount;i++) {
			request.push(requestStack[i].toParamsForBatch());
		}
		
		this.call('batch.process', {'request': request.join(',')}, function(response) {
			callback(new Hyves.ResponseBatch(response));
		});
	}

	/**
	 * createResponse
	 *
	 * returns a Hyves.Response object filled with response data
	 *
	 * @param {object} response, this is the json data returned by Hyves
	 */
	this.createResponse = function(response) {
		return new Hyves.Response(response);
	}

	/**
	 * calculateOAuthSignature
	 *
	 * create the required OAuth signature
	 *
	 * @param {string} method
	 * @param {string} uri
	 * @param {object} params
	 * @param {string} consumerSecret
	 * @param {string} tokenSecret
	 * @return OAuth signature
	 */
	this.calculateOAuthSignature = function(method, uri, params, consumerSecret, tokenSecret) {
		var params = this.OAuth.normalizeKeyValueParameters(params);
		var basestring = this.OAuth.generateBaseString(method, uri, params);
		return this.OAuth.calculateHMACSHA1Signature(basestring, consumerSecret, tokenSecret);
	}
	
	/**
	 * requestAuthorization
	 *
	 * request authorization trough a popup
	 *
	 * @param {string} urlCallback
	 * @param {array} methods
	 * @param {string} expirationtype
	 * @param {function} callbackSuccess
	 * @param {function} callbackError
	 */
	this.requestAuthorization = function(urlCallback, methods, expirationtype, callbackSuccess, callbackError, options) {
		this.hyvesAuth = new Hyves.Authorization(this, callbackSuccess, callbackError, options);
		this.hyvesAuth.request(urlCallback, methods, expirationtype, options);
	}
	
	/**
	 * requestAuthorizationRedirect
	 *
	 * request authorization trough a redirect
	 *
	 * @param {string} urlCallback
	 * @param {array} methods
	 * @param {string} expirationtype
	 * @param {function} callbackSuccess
	 * @param {function} callbackError
	 */
	this.requestAuthorizationRedirect = function(urlCallback, methods, expirationtype, callbackSuccess, callbackError, options) {
		this.hyvesAuth = new Hyves.Authorization(this, callbackSuccess, callbackError, options);
		
		// check for oauth token in url
		var oauthToken = this.getQueryParamValue('oauth_token');
		if(oauthToken !== null) {
			// complete exchange
			this.hyvesAuth.completeRedirect(oauthToken);
		}
		else {
			// redirect user to hyves
			this.hyvesAuth.requestRedirect(urlCallback, methods, expirationtype, options);
		}
	}
	
	/**
	 * saveToken
	 *
	 * save the current access token serialized into a cookie
	 */
	this.saveToken = function() {
		var tokenData = {};
		tokenData['oauth_token'] = this.getToken();
		tokenData['oauth_token_secret'] = this.getTokenSecret();
		tokenData['expiredate'] = this.getTokenExpireDate();
		tokenData['methods'] = this.getTokenMethods().join(',');
		tokenData['userid'] = this.getTokenUserId();
		
		// make it available as long as the tokens are valid
		var seconds = tokenData['expiredate'] - Math.round((new Date()).getTime() / 1000);
		//this.createCookie('hyves_access_token', escape(JSON.stringify(tokenData)), seconds);
		localStorage["hyves_access_token"] =  escape(JSON.stringify(tokenData));
	}

	/**
	 * clearToken
	 *
	 * clears all data from an accesstoken
	 *
	 * @param {object} accesstoken
	 */
	this.clearToken = function() {
		this.setToken('', '');
		this.setTokenExpireDate(0);
		this.setTokenMethods(new Array());
		this.setTokenUserId(0);
	}
	
	/**
	 * restoreToken
	 *
	 * try to restore a token from a cookie
	 *
	 * @param {array} methods
	 * @param {string} testMethod (optional, set to null to skip check)
	 * @param {function} callbackContinue (this will be called when checks succeed)
	 * @param {function} callbackGetToken (this will be called when checks fail)
	 */
	this.restoreToken = function(methods, testMethod, callbackContinue, callbackGetToken) {
		//var token = this.readCookie('hyves_access_token');
		var token = localStorage["hyves_access_token"];
		
		if(token && token !== null) {
			this.parseAccessToken(JSON.parse(unescape(token)));
			
			// @todo check if all required methods are valid for this token
			var methodsValidated = true;
			var tokenValid = false;
			var tokenMethods = this.getTokenMethods();
			var methodsLength = methods.length;
			var tokenMethodsLength = tokenMethods.length;
			for(i = 0; i < methodsLength; i++) {
				var methodFound = false;
				for(j = 0; j < tokenMethodsLength; j++) {
					if(methods[i] == tokenMethods[j]) {
						methodFound = true;
					}
				}
				if(methodFound == false) {
					methodsValidated = false;
					this.clearToken();
					callbackGetToken();
				}
			}

			// optional: test a method to see if the token isn't expired or revoked
			if(testMethod !== null) {
				this.call(testMethod, function(response) {
					try {
						response.parse();
						callbackContinue();
					}
					catch(e) {
						// something wrong with token
						hyves.clearToken();
						callbackGetToken();
					}
				});
			}
			else {
				callbackContinue();
			}
		}
		else {
			this.clearToken();
			callbackGetToken();
		}
	}
	
	
	/**
	 * parseAccessToken
	 *
	 * retrieves the details from the accesstoken and calls the various setters
	 *
	 * @param {object} accesstoken
	 */
	this.parseAccessToken = function(accesstoken) {
		this.setToken(accesstoken.oauth_token, accesstoken.oauth_token_secret);
		this.setTokenExpireDate(accesstoken.expiredate);
		this.setTokenMethods(accesstoken.methods.split(','));
		this.setTokenUserId(accesstoken.userid);
	}
	
	this.getQueryParamValue = function(param) {
		param = param.replace(/[\[]/,"\\\[").replace(/[\]]/,"\\\]");
		var regexS = "[\\?&]" + param + "=([^&#]*)";
		var regex = new RegExp( regexS );
		var results = regex.exec( window.location.href );
		if(results == null) {
			return null;
		}
		return results[1];
	}
	
	// source: http://www.quirksmode.org/js/cookies.html
	// note: changed day to seconds
	this.createCookie = function(name,value,seconds) {
		if (seconds) {
			var date = new Date();
			date.setTime(date.getTime()+(seconds*1000));
			var expires = "; expires="+date.toGMTString();
		}
		else var expires = "";
		document.cookie = name+"="+value+expires+"; path=/";
	}

	this.readCookie = function(name) {
		var nameEQ = name + "=";
		var ca = document.cookie.split(';');
		for(var i=0;i < ca.length;i++) {
			var c = ca[i];
			while (c.charAt(0)==' ') c = c.substring(1,c.length);
			if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length);
		}
		return null;
	}
	
	this.eraseCookie = function(name) {
		this.createCookie(name,"",-1);
	}
	
	// set options if specified
	if(options) {
		if(options.version) {
			this.setVersion(options.version);
		}
		if(options.fancylayout) {
			this.setFancyLayout(options.fancylayout);
		}
	}
}

/**
 * Hyves.Request
 *
 * @param {object} hyves
 * @param {string} method
 * @param {object} params or {function} callback
 * @param {function} callback
 */
Hyves.Request = function(hyves, method, paramsCustom, callback)
{
	this._hyves = hyves;
	this._params = {};

	/**
	 * getCallbackId
	 *
	 * @return integer
	 */
	this.getCallbackId = function() {
		return this._callbackId;
	}

	/**
	 * getCallbackFunction
	 *
	 * @return function
	 */
	this.getCallbackFunction = function() {
		return this._callbackFunction;
	}

	/**
	 * toParams
	 *
	 * return all params from this request to normalized values
	 * and with OAuth parameters
	 *
	 * @return string
	 */
	this.toParams = function() {
		return this._hyves.OAuth.normalizeKeyValueParameters(this.getParams());
	}

	/**
	 * toParamsForBatch
	 *
	 * return all params from this request to normalized values
	 * we don't need the _oauth params and some of the ha_ params
	 * while creating the batch requests
	 *
	 * @return string
	 */
	this.toParamsForBatch = function() {
		var params = this.getParams();

		delete params['ha_callback'];
		delete params['ha_responsecode_always_200'];

		delete params['oauth_consumer_key'];
		delete params['oauth_timestamp'];
		delete params['oauth_nonce'];
		delete params['oauth_signature_method'];
		delete params['oauth_signature'];
		delete params['oauth_token'];

		return this._hyves.OAuth.normalizeKeyValueParameters(params);
	}

	/**
	 * getParams
	 *
	 * get the set of params required for a call to the api
	 *
	 * @return object
	 */
	this.getParams = function() {
		var params = this._params;
		
		// hyves params
		params['ha_method'] = method;
		params['ha_callback'] = 'cbFunc.o' + this.getCallbackId();
		params['ha_version'] = this._hyves._version;
		params['ha_format'] = this._hyves._format;
		params['ha_fancylayout'] = this._hyves._fancylayout;
		params['ha_responsecode_always_200'] = this._hyves._alwaysReturnCode200;

		// oauth params
		params['oauth_consumer_key'] = this._hyves._consumerKey;
		params['oauth_timestamp'] = this._hyves.OAuth.getTimestamp();
		params['oauth_nonce'] = this._hyves.OAuth.getNonce();
		params['oauth_signature_method'] = this._hyves._signatureMethod;

		// add token if specified
		if(this._hyves._token) {
			params['oauth_token'] = this._hyves._token;
		}

		// add calculated signature
		params['oauth_signature'] = this._hyves.calculateOAuthSignature(
			this._hyves._httpRequestMethod,
			this._hyves._urlApi,
			params,
			this._hyves._consumerSecret,
			this._hyves._tokenSecret
		);

		return params;
	}

	// make hybrid calling possible, not all calls require params
	if(typeof paramsCustom == 'function') {
		this._callbackFunction = paramsCustom;
		paramsCustom = {};
	}
	else {
		this._callbackFunction = callback;
	}

	// add custom params if available
	if(paramsCustom) {
		for (x in paramsCustom) {
			this._params[x] = paramsCustom[x];
		}
	}

	// get a callback id for this call
	this._callbackId = this._hyves.getNextCallbackId();
}

/**
 * Hyves.Response
 *
 * @param {string} response: raw json response from Hyves
 */
Hyves.Response = function(response) {
	this._responseRaw = response;
	this._response = response;
	this._isParsed = false;

	this._method = null;
	this._paginated = false,
	this._pagination = null;
	this._info = null;

	this._hasError = false;
	this._errorCode = null;
	this._errorMessage = null;

	/**
	 * isParsed
	 *
	 * check if the content of the response was parsed yet
	 *
	 * @return boolean
	 */
	this.isParsed = function() {
		if(this._isParsed == false) {
			this.parse();
		}
	}

	/**
	 * parse
	 *
	 * parses the response
	 * if an error occured an exception will be thrown
	 *
	 */
	this.parse = function() {
		this._isParsed = true;

		this.checkForErrors(this._response);
		this._info = this._response.info;

		if(this._response.method) {
			this._method = this._response.method;
		}

		if(this._info.currentpage) {
			this._paginated = true;
			this._pagination = {
				currentpage: this._info.currentpage,
				resultsperpage: this._info.resultsperpage,
				totalpages: this._info.totalpages,
				totalresults: this._info.totalresults
			};
		}

		delete this._response.method;
		delete this._response.info;
	}

	/**
	 * getRawData
	 *
	 * @return raw response object
	 */
	this.getRawData = function() {
		return this._responseRaw;
	}

	/**
	 * getData
	 *
	 * @return response data
	 */
	this.getData = function() {
		this.isParsed();
		return this._response;
	}

	/**
	 * getMethod
	 *
	 * @return method
	 */
	this.getMethod = function() {
		this.isParsed();
		return this._method;
	}

	/**
	 * getInfo
	 *
	 * @return the info block from the response
	 */
	this.getInfo = function() {
		this.isParsed();
		return this._info;
	}

	/**
	 * isPaginated
	 *
	 * @return boolean
	 */
	this.isPaginated = function() {
		this.isParsed();
		return this._paginated;
	}

	/**
	 * getPagination
	 *
	 * @return pagination data found in info block
	 */
	this.getPagination = function() {
		this.isParsed();
		return this._pagination;
	}

	/**
	 * checkForErrors
	 *
	 * throw a Hyves_Response_Exception if an error was found
	 */
	this.checkForErrors = function(response) {
		if (typeof(response) !== 'object' || response['error_code']) {
			this._hasError = true;
			this._errorCode = response['error_code'];
			this._errorMessage = response['error_message'].replace('\n', ' ');
			throw new Hyves_Response_Exception(this._errorMessage, this._errorCode, response);
		}
	}

	/**
	 * countResults
	 *
	 * @return integer
	 */
	this.countResults = function(key) {
		var data = this.getData();
		if(data[key]) {
			return data[key].length;
		}
		return 0;
	}

	/**
	 * get
	 *
	 * note: returns empty string if specified data was not found
	 *
	 * @return string
	 */
	this.get = function() {
		var data = this.getData();
		if(!data[arguments[0]]) {
			return '';
		}
		
		var value = data;
		var argumentCount = arguments.length;
		for(ar = 0; ar < argumentCount; ar++) {
			var key = arguments[ar];
			if(value.key) {
				value = value.key;
			}
			else if(value[key]) {
				value = value[key];
			}
		}
		return value;
	}
}

/**
 * Hyves.ResponseBatch
 *
 * @param {string} response: raw json batch response from Hyves
 */
Hyves.ResponseBatch = function(response) {
	this._response = response.getRawData();
	this._isParsed = false;

	this._info = null;

	this._hasError = false;
	this._errorCode = null;
	this._errorMessage = null;

	this._callResponses = new Array();

	/**
	 * isParsed
	 *
	 * check if the content of the response was parsed yet
	 *
	 * @return boolean
	 */
	this.isParsed = function() {
		if(this._isParsed == false) {
			this.parse();
		}
	}

	/**
	 * parse
	 *
	 * parses the response
	 * if an error occured an exception will be thrown
	 *
	 */
	this.parse = function() {
		this._isParsed = true;

		this.checkForErrors(this._response);
		this._info = this._response.info;

		if(this._response.method) {
			this._method = this._response.method;
		}
		
		var callCount = this._response.request.length;
		for(i = 0; i < callCount; i++) {
			var callResponse = this._response.request[i];
			for(var method in callResponse) {}
			this._callResponses.push(new Hyves.Response(callResponse[method]));
		}

		delete this._response.method;
		delete this._response.info;
	}
	this.getData = function() {
		this.isParsed();
		return this._response;
	}
	this.getMethod = function() {
		this.isParsed();
		return this._method;
	}
	this.getInfo = function() {
		this.isParsed();
		return this._info;
	}
	this.getResponse = function(nr) {
		this.isParsed();
		return this._callResponses[nr];
	}
	this.checkForErrors = function(response) {
		if (typeof(response) !== 'object' || response['error_code']) {
			this._hasError = true;
			this._errorCode = response['error_code'];
			this._errorMessage = response['error_message'].replace('\n', ' ');
			throw new Hyves_Response_Batch_Exception(this._errorMessage, this._errorCode, response);
		}
	}
}

/**
 * Hyves.Authorization
 *
 * more about authorization:
 * http://www.hyves-developers.nl/documentation/data-api/hyves-api-user-authorization
 *
 * @param {object} hyves, an instance of the Hyves object
 * @param {function} callbackSuccess
 * @param {function} callbackError
 * @param {object} options
 */
Hyves.Authorization = function(hyves, callbackSuccess, callbackError, options)
{
	this._urlAuthorize = 'http://www.hyves.nl/api/authorize/';
	this._urlAuthorizeMobile = 'http://www.hyves.nl/mini/api/authorize/';

	this._popup = null;
	this._popupName = 'hyvesAuth';
	this._popupWidth = 640;
	this._popupHeight = 480;
	this._popupScrollbars = true;
	this._popupStatus = false;
	this._popupToolbar = false;
	this._popupLocation = false;
	this._mobile = false;

	this._urlCallback = '';
	this._callbackSuccess = callbackSuccess;
	this._callbackError = callbackError;

	this._hyves = hyves;
	this._token = '';
	this._tokenSecret = '';

	/**
	 * setPopupOptions
	 *
	 * @param {object} options
	 */
	this.setPopupOptions = function(options) {
		if(options.popupName) {
			this._popupName = options.popupName;
		}
		if(options.popupWidth) {
			this._popupWidth = options.popupWidth;
		}
		if(options.popupHeight) {
			this._popupHeight = options.popupHeight;
		}
		if(options.popupScrollbars) {
			this._popupScrollbars = options.popupScrollbars;
		}
		if(options.popupStatus) {
			this._popupStatus = options.popupStatus;
		}
		if(options.popupToolbar) {
			this._popupToolbar = options.popupToolbar;
		}
		if(options.popupLocation) {
			this._popupLocation = options.popupLocation;
		}
	}

	/**
	 * getPopupOptions
	 *
	 * @return string with all options combined for window.open
	 */
	this.getPopupOptions = function() {
		var options = new Array();
		options.push('width=' + this._popupWidth);
		options.push('height=' + this._popupHeight);
		options.push('scrollbars=' + this.boolToString(this._popupScrollbars));
		options.push('status=' + this.boolToString(this._popupStatus));
		options.push('toolbar=' + this.boolToString(this._popupToolbar));
		options.push('location=' + this.boolToString(this._popupLocation));
		return options.join(',');
	}
	
	/**
	 * boolToString
	 *
	 * @param {boolean} value
	 * @return yes or no
	 */
	this.boolToString = function(value) {
		if(value == true) {
			return 'yes';
		}
		return 'no';
	}

	/**
	 * getAuthorizeUrl
	 *
	 * get authorization url as defined by Hyves, token and callback are added
	 */
	this.getAuthorizeUrl = function(token, callback)
	{
		var url = this._urlAuthorize;
		if(this.mobile == true) {
			url = this._urlAuthorizeMobile;
		}
		url += '?oauth_token=' + token;
		url += '&oauth_callback=' + this._hyves.OAuth.urlencodeRFC3986_UTF8(callback);
		return url;
	}

	/**
	 * request
	 *
	 * @param {string} urlCallback
	 * @param {array} methods
	 * @param {string} expirationtype (default, infinite or user)
	 * @param {object} options
	 */
	this.request = function(urlCallback, methods, expirationtype, options) {
		this._urlCallback = urlCallback;

		var self = this;

		if(options) {
			if(options.mobile) {
				this._mobile = true;
			}
			this.setPopupOptions(options);
		}

		hyves.call('auth.requesttoken', {
			'methods': methods.join(','),
			'expirationtype': expirationtype
		},
		function(requesttoken) {
			try {
				self._token = requesttoken.get('oauth_token');
				self._tokenSecret = requesttoken.get('oauth_token_secret');

				self._popup = window.open(
					self.getAuthorizeUrl(self._token, self._urlCallback),
					self._popupName, self.getPopupOptions()
				);
			}
			catch(e) {
				self._callbackError(e);
			}
		});
	}
	
	/**
	 * requestRedirect
	 *
	 * @param {string} urlCallback
	 * @param {array} methods
	 * @param {string} expirationtype (default, infinite or user)
	 * @param {object} options
	 */
	this.requestRedirect = function(urlCallback, methods, expirationtype, options) {
		this._urlCallback = urlCallback;
		this._callbackSuccess = callbackSuccess;
		this._callbackError = callbackError;
		var self = this;

		if(options) {
			if(options.mobile) {
				this._mobile = true;
			}
			this.setPopupOptions(options);
		}

		hyves.call('auth.requesttoken', {
			'methods': methods.join(','),
			'expirationtype': expirationtype
		},
		function(requesttoken) {
			try {
				self._token = requesttoken.get('oauth_token');
				self._tokenSecret = requesttoken.get('oauth_token_secret');
				
				// save tokens to cookie
				self._hyves.createCookie('hyves_token', escape(self._token), 300);
				self._hyves.createCookie('hyves_token_secret', escape(self._tokenSecret), 300);
				
				window.location = self.getAuthorizeUrl(self._token, self._urlCallback);
			}
			catch(e) {
				self._callbackError(e);
			}
		});
	}

	/**
	 * complete
	 *
	 * retrieve the accesstoken, this function is being called from the popup
	 */
	this.complete = function() {
		var self = this;
		this._popup.close();
		this._hyves.setToken(this._token, this._tokenSecret);
		this._hyves.call('auth.accesstoken', function(response) {
			try {
				var accesstoken = response.getData();
				self._hyves.parseAccessToken(accesstoken);
				self._callbackSuccess(response);
			}
			catch(error) {
				self._callbackError(error);
			}
		});
	}
	
	/**
	 * completeRedirect
	 *
	 * retrieve the accesstoken, this function is being called when returned from redirect
	 */
	this.completeRedirect = function(token) {
		var self = this;
		var tokenCookie = unescape(this._hyves.readCookie('hyves_token'));
		var tokenSecret = unescape(this._hyves.readCookie('hyves_token_secret'));
		
		this._hyves.eraseCookie('hyves_token');
		this._hyves.eraseCookie('hyves_token_secret');
		
		// see if token matches the returned token from hyves
		if(token && tokenSecret && tokenCookie == unescape(token)) {
			this._token = tokenCookie;
			this._tokenSecret = tokenSecret;
			this._hyves.setToken(this._token, this._tokenSecret);
			this._hyves.call('auth.accesstoken', function(response) {
				try {
					// set access tokens
					var accesstoken = response.getData();
					self._hyves.parseAccessToken(accesstoken);
					self._callbackSuccess(response);
				}
				catch(error) {
					self._callbackError(error);
				}
			});
		}
		else {
			self._callbackError('error reading cookie');
		}
	}
}

// i didn't write this myself :)
var OAuth = function()
{
	this.getTimestamp = function() {
		var timestamp = Math.floor(new Date().valueOf()/1000);
		if (this.timestampLastMethod == timestamp) {
			this.nonce++;
		} else {
			this.timestampLastMethod = timestamp;
			this.nonce = 0;
		}
		return this.timestampLastMethod;
	}
	this.getNonce = function() {
		var rand = Math.ceil(100000*Math.random());
		return this.nonce + "_" + rand;
	}
	this.normalizeParameters = function(params) {
		var paramsEncoded = new Array();
		for (var i = 0; i < params.length; i++) {
			paramsEncoded[paramsEncoded.length] =
				this.urlencodeRFC3986_UTF8(params[i]["key"])
				+ '=' +
				this.urlencodeRFC3986_UTF8(params[i]["value"]);
		}
		paramsEncoded.sort();
		return paramsEncoded.join('&');
	}
	this.normalizeKeyValueParameters = function(paramsInput) {
		var params = new Array();
		for (key in paramsInput) {
			params[params.length] = { key : key, value : paramsInput[key] };
		}
		return this.normalizeParameters(params);
	}
	this.urlencodeRFC3986_UTF8 = function(s) {
		s = encodeURIComponent(s);
        s = s.replace("!", "%21", "g");
        s = s.replace("*", "%2A", "g");
        s = s.replace("'", "%27", "g");
        s = s.replace("(", "%28", "g");
        s = s.replace(")", "%29", "g");
		return s;
	}
	this.generateBaseString = function(httpRequestMethod, uri, params) {
		var basestring = new Array(
			this.urlencodeRFC3986_UTF8(httpRequestMethod),
			this.urlencodeRFC3986_UTF8(uri),
			this.urlencodeRFC3986_UTF8(params)
		);
		return basestring.join('&');
	}
	this.calculateHMACSHA1Signature = function(basestring, consumerSecret, tokenSecret) {
		var key = new Array(
			this.urlencodeRFC3986_UTF8(consumerSecret),
			this.urlencodeRFC3986_UTF8(tokenSecret)
		);
		key = key.join('&');
		return encode64(str_hmac_sha1(key, basestring));
	}
}

/**
 * JSON implementation by Craig Buckler, Optimalworks.net
 * var str = JSON.stringify(object);
 * var obj = JSON.parse(str);
 */
var JSON = JSON || {};
JSON.stringify = JSON.stringify || function (obj) {  
	var t = typeof (obj);  
	if (t != "object" || obj === null) {  
		// simple data type  
		if (t == "string") obj = '"'+obj+'"';  
		return String(obj);  
	}  
	else {  
		// recurse array or object  
		var n, v, json = [], arr = (obj && obj.constructor == Array);  
		for (n in obj) {  
			v = obj[n]; t = typeof(v);  
			if (t == "string") v = '"'+v+'"';  
			else if (t == "object" && v !== null) v = JSON.stringify(v);  
			json.push((arr ? "" : '"' + n + '":') + String(v));  
		}  
		return (arr ? "[" : "{") + String(json) + (arr ? "]" : "}");  
	}
};
JSON.parse = JSON.parse || function (str) {  
	if (str === "") str = '""';  
	eval("var p=" + str + ";");  
	return p;  
};

/**
 * Exceptions
 */
function Hyves_Exception(message, code) {
	this.message = message;
	this.code = code;
}
Hyves_Exception.prototype.getCode = function(){
	return this.code;
}
Hyves_Exception.prototype.getMessage = function(){
	return this.message;
}
Hyves_Exception.prototype.getType = function(){
	return 'Hyves_Exception';
}

function Hyves_Request_Exception(message, code) {
	this.base = Hyves_Exception;
	this.base(message, code);
}
Hyves_Request_Exception.prototype = new Hyves_Exception;
Hyves_Response_Exception.prototype.getType = function(){
	return 'Hyves_Request_Exception';
}

function Hyves_Response_Exception(message, code, response) {
	this.base = Hyves_Exception;
	this.base(message, code);
	this.response = response;
}
Hyves_Response_Exception.prototype = new Hyves_Exception;
Hyves_Response_Exception.prototype.getResponse = function(){
	return this.response;
}
Hyves_Response_Exception.prototype.getType = function(){
	return 'Hyves_Response_Exception';
}

function Hyves_Response_Batch_Exception(message, code, response) {
	this.base = Hyves_Response_Exception;
	this.base(message, code);
	this.response = response;
}
Hyves_Response_Batch_Exception.prototype = new Hyves_Response_Exception;
Hyves_Response_Batch_Exception.prototype.getResponse = function(){
	return this.response;
}
Hyves_Response_Batch_Exception.prototype.getType = function(){
	return 'Hyves_Response_Batch_Exception';
}